2
0
laowu 8 часов назад
Родитель
Сommit
9563f61cf6

+ 117 - 0
app/Console/Commands/CheckIosAppStore.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Notification\TelegramBot;
+use App\Util;
+use Illuminate\Console\Command;
+
+/**
+ * 检测配置的 iOS App Store 包是否被下架,若下架则通过 Telegram 通知。
+ */
+class CheckIosAppStore extends Command
+{
+    protected $signature = 'ios:check-app-store';
+
+    protected $description = '检测 iOS App Store 包是否被下架,下架时 Telegram 通知';
+
+    /** 要检测的 iOS 包列表:name => url */
+    protected $apps = [
+
+    ];
+
+    /** 页面内容中表示“已下架”的关键词(不区分大小写) */
+    protected $delistedKeywords = [
+        'no longer available',
+        'removed this app from the app store',
+        'not available',
+        'this app is no longer',
+        'has been removed',
+    ];
+
+    public function handle()
+    {
+        $env = env('APP_ENV', 'local');
+        $delisted = [];
+
+        foreach ($this->apps as $name => $url) {
+            $result = $this->checkApp($url);
+            if ($result['delisted']) {
+                $delisted[] = [
+                    'name' => $name,
+                    'url'  => $url,
+                    'reason' => $result['reason'],
+                ];
+                Util::WriteLog('ios_app_store_check', "Delisted: {$name} | {$url} | {$result['reason']}");
+            }
+        }
+
+        if (count($delisted) > 0) {
+            $this->notifyDelisted($env, $delisted);
+            $this->warn('已检测到 ' . count($delisted) . ' 个包被下架,已发送 Telegram 通知。');
+            return 1;
+        }
+
+        $this->info('所有 iOS 包状态正常。');
+        return 0;
+    }
+
+    /**
+     * 检测单个 App 是否可访问(未下架)
+     * @return array ['delisted' => bool, 'reason' => string]
+     */
+    protected function checkApp($url)
+    {
+        $ch = curl_init($url);
+        curl_setopt_array($ch, [
+            CURLOPT_RETURNTRANSFER => true,
+            CURLOPT_FOLLOWLOCATION => true,
+            CURLOPT_TIMEOUT       => 15,
+            CURLOPT_USERAGENT     => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+            CURLOPT_SSL_VERIFYPEER => true,
+        ]);
+        $body = curl_exec($ch);
+        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        $error = curl_error($ch);
+        curl_close($ch);
+
+        if ($error) {
+            return ['delisted' => true, 'reason' => "请求失败: {$error}"];
+        }
+
+        if ($code === 404) {
+            return ['delisted' => true, 'reason' => 'HTTP 404'];
+        }
+
+        if ($code !== 200) {
+            return ['delisted' => true, 'reason' => "HTTP {$code}"];
+        }
+
+        $bodyLower = mb_strtolower($body);
+        foreach ($this->delistedKeywords as $keyword) {
+            if (strpos($bodyLower, $keyword) !== false) {
+                return ['delisted' => true, 'reason' => "页面包含下架提示: {$keyword}"];
+            }
+        }
+
+        return ['delisted' => false, 'reason' => ''];
+    }
+
+    protected function notifyDelisted($env, array $delisted)
+    {
+        $lines = ["[{$env}] iOS App Store 下架检测告警"];
+        foreach ($delisted as $item) {
+            $lines[] = "• {$item['name']}";
+            $lines[] = "  原因: {$item['reason']}";
+            $lines[] = "  链接: {$item['url']}";
+        }
+        $message = implode("\n", $lines);
+
+        try {
+            TelegramBot::getDefault()->sendMsg($message);
+        } catch (\Exception $e) {
+            Util::WriteLog('ios_app_store_check', 'Telegram send error: ' . $e->getMessage());
+            $this->error('Telegram 发送失败: ' . $e->getMessage());
+        }
+    }
+}

+ 4 - 0
app/Console/Kernel.php

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

+ 16 - 6
app/Http/Controllers/Admin/CommonConfigController.php

@@ -10,9 +10,7 @@ class CommonConfigController extends Controller
 {
     public function index()
     {
-        // 从 Redis 获取公用配置数据
-        $configData = Redis::get("GameConfigX_Common");
-        $config = $configData ? json_decode($configData, true) : [
+        $defaultConfig = [
             ["RechargeMin" => 0, "RechargeMax" => 1, "FreeWinMax" => 1000, "RechargeMaxPercent" => 0, "TaxPercent" => 100],
             ["RechargeMin" => 1, "RechargeMax" => 101, "FreeWinMax" => 20, "RechargeMaxPercent" => 80, "TaxPercent" => 100],
             ["RechargeMin" => 101, "RechargeMax" => 501, "FreeWinMax" => 100, "RechargeMaxPercent" => 70, "TaxPercent" => 95],
@@ -21,14 +19,26 @@ class CommonConfigController extends Controller
             ["RechargeMin" => 10001, "RechargeMax" => 99999999, "FreeWinMax" => 0, "RechargeMaxPercent" => 50, "TaxPercent" => 80]
         ];
 
-        return view('admin.common_config.index', compact('config'));
+        // 从 Redis 获取公用配置数据
+        $configData = Redis::get("GameConfigX_Common");
+        $config = $configData ? json_decode($configData, true) : $defaultConfig;
+
+        // 从 Redis 获取库存模式配置数据
+        $specialConfigData = Redis::get("GameConfigX_Special");
+        $specialConfig = $specialConfigData ? json_decode($specialConfigData, true) : $defaultConfig;
+
+        return view('admin.common_config.index', compact('config', 'specialConfig'));
     }
 
     public function update(Request $request)
     {
         $config = $request->input('config');
+        $configType = $request->input('config_type', 'common'); // 获取配置类型,默认为 common
+
+        $redisKey = $configType === 'special' ? 'GameConfigX_Special' : 'GameConfigX_Common';
 
         \Log::info('公用配置更新请求', [
+            'config_type' => $configType,
             'config' => $config,
             'all_data' => $request->all()
         ]);
@@ -45,8 +55,8 @@ class CommonConfigController extends Controller
                 throw new \Exception('JSON格式错误: ' . json_last_error_msg());
             }
 
-            Redis::set("GameConfigX_Common", $config);
-            \Log::info('公用配置更新成功');
+            Redis::set($redisKey, $config);
+            \Log::info('公用配置更新成功', ['redis_key' => $redisKey]);
             return response()->json(['status' => 'success', 'message' => '更新成功']);
         } catch (\Exception $e) {
             \Log::error('公用配置更新失败:异常', ['message' => $e->getMessage()]);

+ 29 - 2
app/Http/Controllers/Admin/GameDataController.php

@@ -568,9 +568,36 @@ class GameDataController extends Controller
             ->leftJoin('QPTreasureDB.dbo.GameScoreInfo as gsi','gl.UserID','=','gsi.UserID')
             ->where($where)
             ->selectRaw('ai.GameID,ai.NickName,Channel,gi.ServerName,gi.SortID,gl.CollectDate,gl.UserID,gsi.Score')
-//            ->orderByDesc('CreateTime')
+            // 按进入游戏时间倒序
+            ->orderByDesc('gl.CollectDate')
             ->paginate(10);
-
+        // 计算库存模式:总充值 > StockMode2SwitchRecharge
+        if ($list->count() > 0) {
+            $userIds = [];
+            foreach ($list as $item) {
+                $userIds[] = $item->UserID;
+            }
+            $userIds = array_unique($userIds);
+
+            // 获取模式切换充值阈值
+            $switchRecharge = DB::connection('read')
+                ->table('QPAccountsDB.dbo.SystemStatusInfo')
+                ->where('StatusName', 'StockMode2SwitchRecharge')
+                ->value('StatusValue');
+            $switchRecharge = $switchRecharge ?? 0;
+
+            // 获取用户总充值(YN_VIPAccount.Recharge)
+            $rechargeMap = DB::connection('read')
+                ->table('QPAccountsDB.dbo.YN_VIPAccount')
+                ->whereIn('UserID', $userIds)
+                ->pluck('Recharge', 'UserID')
+                ->toArray();
+
+            foreach ($list as $item) {
+                $totalRecharge = $rechargeMap[$item->UserID] ?? 0;
+                $item->is_stock_mode = $totalRecharge > $switchRecharge ? 1 : 0;
+            }
+        }
         $games = DB::connection('read')->table(TableName::QPPlatformDB() . 'GameKindItem')
             ->whereIn('GameID', config('games.openKGame'))
             ->select('KindName', 'GameID')

+ 9 - 6
app/Http/Controllers/Admin/GameSomeConfigController.php

@@ -15,24 +15,26 @@ class GameSomeConfigController
     {
         // 获取游戏ID,默认为0(通用配置)
         $gameId = $request->input('game_id', 0);
-        
+        // 获取库存模式,默认为1(普通模式)
+        $stockMode = $request->input('stock_mode', 1);
         // 可选的游戏列表
         $games = [
             0 => '通用配置',
             '921219001' => 'IGT-双砖',
             '1519119693' => 'Jokers Jewels',
         ];
-        
-        // 查询指定GameID的配置
+
+        // 查询指定GameID和StockMode的配置
         $configs = DB::connection('write')
             ->table('QPPlatformDB.dbo.GameSomeConfig')
             ->where('GameID', $gameId)
+            ->where('StockMode', $stockMode)
             ->orderBy('ZMin', 'asc')
             ->orderBy('ZMax', 'asc')
             ->orderBy('MultiMin', 'asc')
             ->get();
 
-        return view('admin.game_some_config.index', compact('configs', 'games', 'gameId'));
+        return view('admin.game_some_config.index', compact('configs', 'games', 'gameId', 'stockMode'));
     }
 
     /**
@@ -74,8 +76,9 @@ class GameSomeConfigController
                 if (isset($data['Weight'])) {
                     $updateData['Weight'] = (int)$data['Weight'];
                 }
-                if (isset($data['WeightAdjust'])) {
-                    $updateData['WeightAdjust'] = $data['WeightAdjust'];
+                // 个控调节支持配置为空(使用 array_key_exists 以支持空字符串)
+                if (array_key_exists('WeightAdjust', $data)) {
+                    $updateData['WeightAdjust'] = (string)($data['WeightAdjust'] ?? '');
                 }
                 
                 // Status字段不允许修改,已移除

+ 341 - 0
app/Http/Controllers/Admin/StockModeController.php

@@ -0,0 +1,341 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+
+class StockModeController extends Controller
+{
+    /**
+     * 显示库存模式配置页面
+     */
+    public function index()
+    {
+        // 获取 SystemStatusInfo 中的配置
+        $systemConfig = DB::connection('write')
+            ->table('QPAccountsDB.dbo.SystemStatusInfo')
+            ->whereIn('StatusName', ['StockMode2BetLevels', 'StockMode2RevenueRatio', 'StockMode2SwitchRecharge'])
+            ->get()
+            ->keyBy('StatusName');
+
+        // 解析 StockMode2BetLevels
+        $betLevelsString = $systemConfig['StockMode2BetLevels']->StatusString ?? '0|2|10|10000,0|0|100|1000';
+        $betLevelsParts = explode(',', $betLevelsString);
+        $betMaxLimits = explode('|', $betLevelsParts[0] ?? '0|2|10|10000');
+        $rechargeMinLimits = explode('|', $betLevelsParts[1] ?? '0|0|100|1000');
+
+        // 获取 RoomStockStatic2 数据
+        $roomStocks = DB::connection('write')
+            ->table('QPPlatformDB.dbo.RoomStockStatic2')
+            ->where('GameID', 0)
+            ->orderBy('SortID')
+            ->get();
+
+        return view('admin.stock_mode.index', compact(
+            'systemConfig',
+            'betMaxLimits',
+            'rechargeMinLimits',
+            'roomStocks'
+        ));
+    }
+
+    /**
+     * 更新系统配置参数
+     */
+    public function updateSystemConfig(Request $request)
+    {
+        try {
+            $betMaxLimits = $request->input('bet_max_limits', []);
+            $rechargeMinLimits = $request->input('recharge_min_limits', []);
+            $revenueRatio = $request->input('revenue_ratio', 50);
+            $switchRecharge = $request->input('switch_recharge', 100);
+
+            // 构建 StockMode2BetLevels 的 StatusString
+            $betMaxString = implode('|', $betMaxLimits);
+            $rechargeMinString = implode('|', $rechargeMinLimits);
+            $betLevelsString = $betMaxString . ',' . $rechargeMinString;
+
+            // 更新 StockMode2BetLevels
+            DB::connection('write')
+                ->table('QPAccountsDB.dbo.SystemStatusInfo')
+                ->where('StatusName', 'StockMode2BetLevels')
+                ->update([
+                    'StatusString' => $betLevelsString,
+                    'StatusValue' => 0
+                ]);
+
+            // 更新 StockMode2RevenueRatio
+            DB::connection('write')
+                ->table('QPAccountsDB.dbo.SystemStatusInfo')
+                ->where('StatusName', 'StockMode2RevenueRatio')
+                ->update([
+                    'StatusValue' => $revenueRatio
+                ]);
+
+            // 更新 StockMode2SwitchRecharge
+            DB::connection('write')
+                ->table('QPAccountsDB.dbo.SystemStatusInfo')
+                ->where('StatusName', 'StockMode2SwitchRecharge')
+                ->update([
+                    'StatusValue' => $switchRecharge
+                ]);
+
+            \Log::info('库存模式系统配置更新成功', [
+                'bet_levels' => $betLevelsString,
+                'revenue_ratio' => $revenueRatio,
+                'switch_recharge' => $switchRecharge
+            ]);
+
+            return response()->json(['status' => 'success', 'message' => '系统配置更新成功']);
+        } catch (\Exception $e) {
+            \Log::error('库存模式系统配置更新失败', ['error' => $e->getMessage()]);
+            return response()->json(['status' => 'error', 'message' => '更新失败: ' . $e->getMessage()]);
+        }
+    }
+
+    /**
+     * 更新房间库存配置
+     */
+    public function updateRoomStock(Request $request)
+    {
+        try {
+            $sortId = $request->input('sort_id');
+            $levelBase = $request->input('level_base');
+
+            if (!$sortId || !is_numeric($levelBase)) {
+                return response()->json(['status' => 'error', 'message' => '参数错误']);
+            }
+
+            // 更新 RoomStockStatic2
+            $affected = DB::connection('write')
+                ->table('QPPlatformDB.dbo.RoomStockStatic2')
+                ->where('GameID', 0)
+                ->where('SortID', $sortId)
+                ->update([
+                    'LevelBase' => $levelBase
+                ]);
+
+            if ($affected === 0) {
+                // 如果没有记录,则插入新记录
+                DB::connection('write')
+                    ->table('QPPlatformDB.dbo.RoomStockStatic2')
+                    ->insert([
+                        'GameID' => 0,
+                        'SortID' => $sortId,
+                        'Stock' => 0,
+                        'LevelBase' => $levelBase,
+                        'Revenue' => 0,
+                        'RevenueD' => 0
+                    ]);
+            }
+
+            \Log::info('房间库存配置更新成功', [
+                'sort_id' => $sortId,
+                'level_base' => $levelBase
+            ]);
+
+            return response()->json(['status' => 'success', 'message' => '房间库存配置更新成功']);
+        } catch (\Exception $e) {
+            \Log::error('房间库存配置更新失败', ['error' => $e->getMessage()]);
+            return response()->json(['status' => 'error', 'message' => '更新失败: ' . $e->getMessage()]);
+        }
+    }
+
+    /**
+     * 更新库存(增减)
+     */
+    public function updateStock(Request $request)
+    {
+        try {
+            $sortId = $request->input('sort_id');
+            $adjustValue = $request->input('adjust_value'); // 已经是数据库值(前端已乘以100)
+
+            if (!$sortId || !is_numeric($adjustValue)) {
+                return response()->json(['status' => 'error', 'message' => '参数错误']);
+            }
+
+            // 使用 increment/decrement 方法更新库存
+            $query = DB::connection('write')
+                ->table('QPPlatformDB.dbo.RoomStockStatic2')
+                ->where('GameID', 0)
+                ->where('SortID', $sortId);
+
+            if ($adjustValue > 0) {
+                $query->increment('Stock', $adjustValue);
+            } elseif ($adjustValue < 0) {
+                $query->decrement('Stock', abs($adjustValue));
+            }
+
+            // 获取更新后的库存值
+            $roomStock = DB::connection('write')
+                ->table('QPPlatformDB.dbo.RoomStockStatic2')
+                ->where('GameID', 0)
+                ->where('SortID', $sortId)
+                ->first();
+
+            if (!$roomStock) {
+                return response()->json(['status' => 'error', 'message' => '房间数据不存在']);
+            }
+
+            \Log::info('库存更新成功', [
+                'sort_id' => $sortId,
+                'adjust_value' => $adjustValue,
+                'new_stock' => $roomStock->Stock
+            ]);
+
+            return response()->json([
+                'status' => 'success',
+                'message' => '库存更新成功',
+                'new_stock' => $roomStock->Stock
+            ]);
+        } catch (\Exception $e) {
+            \Log::error('库存更新失败', ['error' => $e->getMessage()]);
+            return response()->json(['status' => 'error', 'message' => '更新失败: ' . $e->getMessage()]);
+        }
+    }
+
+    /**
+     * 获取房间库存统计信息
+     */
+    public function getRoomStockStats()
+    {
+        try {
+            $roomStocks = DB::connection('write')
+                ->table('QPPlatformDB.dbo.RoomStockStatic2')
+                ->where('GameID', 0)
+                ->orderBy('SortID')
+                ->get();
+
+            return response()->json(['status' => 'success', 'data' => $roomStocks]);
+        } catch (\Exception $e) {
+            \Log::error('获取房间库存统计失败', ['error' => $e->getMessage()]);
+            return response()->json(['status' => 'error', 'message' => '获取失败: ' . $e->getMessage()]);
+        }
+    }
+
+    /**
+     * 获取库存快照历史数据
+     */
+    public function getSnapshotHistory(Request $request)
+    {
+        try {
+            $sortId = $request->input('sort_id', 1);
+            $minutes = $request->input('minutes', 60); // 默认1小时
+
+            // 计算开始时间
+            $startTime = date('Y-m-d H:i:s', strtotime("-{$minutes} minutes"));
+
+            // 查询起始时间之前的最后一条数据(用于填充前置空白)
+            $previousSnapshot = DB::connection('write')
+                ->table('QPPlatformDB.dbo.RoomStockSnapshot2')
+                ->where('GameID', 0)
+                ->where('SortID', $sortId)
+                ->where('CreateTime', '<', $startTime)
+                ->orderBy('CreateTime', 'desc')
+                ->first();
+
+            // 查询时间范围内的快照数据
+            $snapshots = DB::connection('write')
+                ->table('QPPlatformDB.dbo.RoomStockSnapshot2')
+                ->where('GameID', 0)
+                ->where('SortID', $sortId)
+                ->where('CreateTime', '>=', $startTime)
+                ->orderBy('CreateTime', 'asc')
+                ->get();
+
+            // 处理数据
+            $data = [];
+
+            // 添加前置数据(如果存在)
+            if ($previousSnapshot) {
+                $stock = $previousSnapshot->Stock;
+                $levelBase = $previousSnapshot->LevelBase;
+                $stockRatio = $levelBase > 0 ? round($stock / $levelBase, 2) : 0;
+                $zValue = $this->calculateZValue($stock, $levelBase);
+
+                $data[] = [
+                    'id' => $previousSnapshot->ID,
+                    'stock' => round($stock / 100, 2),
+                    'levelBase' => round($levelBase / 100, 2),
+                    'revenue' => round($previousSnapshot->Revenue / 100, 2),
+                    'revenueD' => round($previousSnapshot->RevenueD / 100, 2),
+                    'stockChange' => round($previousSnapshot->StockChange / 100, 2),
+                    'revenueChange' => round($previousSnapshot->RevenueChange / 100, 2),
+                    'stockRatio' => $stockRatio,
+                    'zValue' => $zValue,
+                    'createTime' => $previousSnapshot->CreateTime,
+                    'timestamp' => strtotime($previousSnapshot->CreateTime) * 1000,
+                    'isPrevious' => true // 标记为前置数据
+                ];
+            }
+
+            // 添加时间范围内的数据
+            foreach ($snapshots as $snapshot) {
+                $stock = $snapshot->Stock;
+                $levelBase = $snapshot->LevelBase;
+                $stockRatio = $levelBase > 0 ? round($stock / $levelBase, 2) : 0;
+                $zValue = $this->calculateZValue($stock, $levelBase);
+
+                $data[] = [
+                    'id' => $snapshot->ID,
+                    'stock' => round($stock / 100, 2),
+                    'levelBase' => round($levelBase / 100, 2),
+                    'revenue' => round($snapshot->Revenue / 100, 2),
+                    'revenueD' => round($snapshot->RevenueD / 100, 2),
+                    'stockChange' => round($snapshot->StockChange / 100, 2),
+                    'revenueChange' => round($snapshot->RevenueChange / 100, 2),
+                    'stockRatio' => $stockRatio,
+                    'zValue' => $zValue,
+                    'createTime' => $snapshot->CreateTime,
+                    'timestamp' => strtotime($snapshot->CreateTime) * 1000,
+                    'isPrevious' => false
+                ];
+            }
+
+            return response()->json(['status' => 'success', 'data' => $data]);
+        } catch (\Exception $e) {
+            \Log::error('获取快照历史失败', ['error' => $e->getMessage()]);
+            return response()->json(['status' => 'error', 'message' => '获取失败: ' . $e->getMessage()]);
+        }
+    }
+
+    /**
+     * 计算 Z 值(个控)
+     */
+    private function calculateZValue($stock, $levelBase)
+    {
+        if ($levelBase == 0) {
+            return 0;
+        }
+
+        if ($stock < $levelBase * 2) {
+            return 0;
+        } elseif ($stock >= $levelBase * 2 && $stock < $levelBase * 4) {
+            // 2x ~ 4x 区间:Z = FLOOR((stock - 2*base) / (2*base / 5)) + 1
+            $stockDiff = $stock - ($levelBase * 2);
+            $baseStep = $levelBase * 2 / 5;
+            $zValue = floor($stockDiff / $baseStep) + 1;
+            return min(max($zValue, 1), 5);
+        } elseif ($stock >= $levelBase * 4 && $stock < $levelBase * 6) {
+            // 4x ~ 6x 区间
+            $stockDiff = $stock - ($levelBase * 4);
+            $baseStep = $levelBase * 2 / 5;
+            $zValue = floor($stockDiff / $baseStep) + 6;
+            return min(max($zValue, 6), 10);
+        } elseif ($stock >= $levelBase * 6 && $stock < $levelBase * 8) {
+            // 6x ~ 8x 区间
+            $stockDiff = $stock - ($levelBase * 6);
+            $baseStep = $levelBase * 2 / 5;
+            $zValue = floor($stockDiff / $baseStep) + 11;
+            return min(max($zValue, 11), 15);
+        } else {
+            // 8x+ 区间
+            $stockDiff = $stock - ($levelBase * 8);
+            $baseStep = $levelBase * 4 / 5;
+            $zValue = floor($stockDiff / $baseStep) + 16;
+            return min(max($zValue, 16), 20);
+        }
+    }
+}

+ 8 - 8
app/Http/Controllers/Admin/WithdrawalController.php

@@ -531,14 +531,14 @@ class WithdrawalController extends BaseController
         $getUserTab = StoredProcedure::getUserTab($UserID);
         $TabType = $getUserTab[0]->TabType ?? '';
 
-        if (!empty($getUserTab) && $TabType == 1) {
-
-            $Type12 = $getUserTab[0]->Type12 ?? 0;
-
-            $userTab = DB::connection('read')->table('QPAccountsDB.dbo.AccountsTabExplain as el')
-                ->where('TabID', $Type12)
-                ->first()->TypeName;
-        }
+//        if (!empty($getUserTab) && $TabType == 1) {
+//
+//            $Type12 = $getUserTab[0]->Type12 ?? 0;
+//
+//            $userTab = DB::connection('read')->table('QPAccountsDB.dbo.AccountsTabExplain as el')
+//                ->where('TabID', $Type12)
+//                ->first()->TypeName;
+//        }
 
         return view('admin.Withdrawal.risk_assessment', [
             'info' => $info,

+ 4 - 2
app/Http/Controllers/Game/ActivityController.php

@@ -84,7 +84,8 @@ class ActivityController extends Controller
                 'user_id' => $user->UserID,
                 'error' => $e->getMessage(),
             ]);
-            return apiReturnFail($e->getMessage());
+            // 这里统一返回通用错误,不向前端抛出具体异常信息
+            return apiReturnFail(['web.activity.wheel_error', __('messages.web.activity.wheel_error')]);
         }
     }
 
@@ -121,7 +122,8 @@ class ActivityController extends Controller
                 'user_id' => $user->UserID,
                 'error' => $e->getMessage(),
             ]);
-            return apiReturnFail($e->getMessage());
+            // 这里统一返回通用错误,不向前端抛出具体异常信息
+            return apiReturnFail(['web.activity.wheel_error', __('messages.web.activity.wheel_error')]);
         }
     }
     public function List(Request $request)

+ 9 - 1
resources/lang/en/messages.php

@@ -252,7 +252,15 @@ return [
         "activity" => [
             "login_required" => "Please log in first",
             "redpack_not_available" => "There is no RedPack available",
-            "redpack_no_recharge" => "You need to deposit first!"
+            "redpack_no_recharge" => "You need to deposit first!",
+            "wheel_error" => "Spin failed, please try again later",
+            "wheel_too_frequent" => "Operation too frequent, please try again later",
+            "wheel_not_open" => "Activity is not open",
+            "wheel_not_started" => "Activity has not started",
+            "wheel_ended" => "Activity has ended",
+            "wheel_config_missing" => "Wheel configuration does not exist",
+            "wheel_no_times" => "No available spin times",
+            "wheel_weight_error" => "Wheel weight configuration error"
         ],
         "protect" => [
             "score_limit" => "Score limit reached",

+ 8 - 0
resources/lang/zh/web.php

@@ -75,6 +75,14 @@ return [
     'activity' => [
         'redpack_not_available' => '没有红包可用',
         'redpack_no_recharge' => '您需要先充值!',
+        'wheel_error' => '抽奖失败,请稍后再试',
+        'wheel_too_frequent' => '操作太频繁,请稍后再试',
+        'wheel_not_open' => '活动未开启',
+        'wheel_not_started' => '活动未开始',
+        'wheel_ended' => '活动已结束',
+        'wheel_config_missing' => '转盘配置不存在',
+        'wheel_no_times' => '没有可用的转盘次数',
+        'wheel_weight_error' => '转盘权重配置错误',
     ],
     'gift' => [
         'config_not_exists' => '礼包配置不存在',

+ 149 - 61
resources/views/admin/common_config/index.blade.php

@@ -10,66 +10,151 @@
                     <small class="text-muted">编辑 6 档参数并一键保存 (索引 0-5)</small>
                 </div>
                 <div class="card-body">
-                    <div class="table-responsive">
-                        <table class="table table-striped table-hover align-middle mb-0" style="min-width: 900px;">
-                            <thead class="thead-light">
-                                <tr>
-                                    <th style="width:82px;">档位</th>
-                                    <th style="width:180px;">充值区间下限 (RechargeMin)</th>
-                                    <th style="width:180px;">充值区间上限 (RechargeMax)</th>
-                                    <th style="width:180px;">固定值 (FreeWinMax)</th>
-                                    <th style="width:180px;">充值最大百分比 (RechargeMaxPercent)</th>
-                                    <th style="width:160px;">税收比例 (TaxPercent)</th>
-                                </tr>
-                            </thead>
-                            <tbody class="config-form">
-                                @foreach([0,1,2,3,4,5] as $index => $level)
-                                    <tr>
-                                        <td>
-                                            <span class="badge badge-primary">等级{{ $level }}</span>
-                                        </td>
-                                        <td>
-                                            <input type="number" class="form-control form-control-sm" 
-                                                   name="level[{{ $level }}][RechargeMin]" 
-                                                   value="{{ $config[$level]['RechargeMin'] ?? 0 }}" />
-                                        </td>
-                                        <td>
-                                            <input type="number" class="form-control form-control-sm" 
-                                                   name="level[{{ $level }}][RechargeMax]" 
-                                                   value="{{ $config[$level]['RechargeMax'] ?? 0 }}" />
-                                        </td>
-                                        <td>
-                                            <input type="number" class="form-control form-control-sm" 
-                                                   name="level[{{ $level }}][FreeWinMax]" 
-                                                   value="{{ $config[$level]['FreeWinMax'] ?? 0 }}" />
-                                        </td>
-                                        <td>
-                                            <div class="input-group input-group-sm">
-                                                <input type="number" class="form-control" 
-                                                       name="level[{{ $level }}][RechargeMaxPercent]" 
-                                                       value="{{ $config[$level]['RechargeMaxPercent'] ?? 0 }}" />
-                                                <div class="input-group-append"><span class="input-group-text">%</span></div>
-                                            </div>
-                                        </td>
-                                        <td>
-                                            <div class="input-group input-group-sm">
-                                                <input type="number" class="form-control" 
-                                                       name="level[{{ $level }}][TaxPercent]" 
-                                                       value="{{ $config[$level]['TaxPercent'] ?? 0 }}" />
-                                                <div class="input-group-append"><span class="input-group-text">%</span></div>
-                                            </div>
-                                        </td>
-                                    </tr>
-                                @endforeach
-                            </tbody>
-                        </table>
-                    </div>
+                    <!-- 标签页导航 -->
+                    <ul class="nav nav-tabs mb-3" role="tablist">
+                        <li class="nav-item">
+                            <a class="nav-link active" data-toggle="tab" href="#common-config" role="tab">
+                                <i class="mdi mdi-cog"></i> 常规配置
+                            </a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" data-toggle="tab" href="#special-config" role="tab">
+                                <i class="mdi mdi-package-variant"></i> 库存模式
+                            </a>
+                        </li>
+                    </ul>
+
+                    <!-- 标签页内容 -->
+                    <div class="tab-content">
+                        <!-- 常规配置标签页 -->
+                        <div class="tab-pane fade show active" id="common-config" role="tabpanel">
+                            <div class="table-responsive">
+                                <table class="table table-striped table-hover align-middle mb-0" style="min-width: 900px;">
+                                    <thead class="thead-light">
+                                        <tr>
+                                            <th style="width:82px;">档位</th>
+                                            <th style="width:180px;">充值区间下限 (RechargeMin)</th>
+                                            <th style="width:180px;">充值区间上限 (RechargeMax)</th>
+                                            <th style="width:180px;">固定值 (FreeWinMax)</th>
+                                            <th style="width:180px;">充值最大百分比 (RechargeMaxPercent)</th>
+                                            <th style="width:160px;">税收比例 (TaxPercent)</th>
+                                        </tr>
+                                    </thead>
+                                    <tbody class="config-form" data-config-type="common">
+                                        @foreach([0,1,2,3,4,5] as $index => $level)
+                                            <tr>
+                                                <td>
+                                                    <span class="badge badge-primary">等级{{ $level }}</span>
+                                                </td>
+                                                <td>
+                                                    <input type="number" class="form-control form-control-sm"
+                                                           name="level[{{ $level }}][RechargeMin]"
+                                                           value="{{ $config[$level]['RechargeMin'] ?? 0 }}" />
+                                                </td>
+                                                <td>
+                                                    <input type="number" class="form-control form-control-sm"
+                                                           name="level[{{ $level }}][RechargeMax]"
+                                                           value="{{ $config[$level]['RechargeMax'] ?? 0 }}" />
+                                                </td>
+                                                <td>
+                                                    <input type="number" class="form-control form-control-sm"
+                                                           name="level[{{ $level }}][FreeWinMax]"
+                                                           value="{{ $config[$level]['FreeWinMax'] ?? 0 }}" />
+                                                </td>
+                                                <td>
+                                                    <div class="input-group input-group-sm">
+                                                        <input type="number" class="form-control"
+                                                               name="level[{{ $level }}][RechargeMaxPercent]"
+                                                               value="{{ $config[$level]['RechargeMaxPercent'] ?? 0 }}" />
+                                                        <div class="input-group-append"><span class="input-group-text">%</span></div>
+                                                    </div>
+                                                </td>
+                                                <td>
+                                                    <div class="input-group input-group-sm">
+                                                        <input type="number" class="form-control"
+                                                               name="level[{{ $level }}][TaxPercent]"
+                                                               value="{{ $config[$level]['TaxPercent'] ?? 0 }}" />
+                                                        <div class="input-group-append"><span class="input-group-text">%</span></div>
+                                                    </div>
+                                                </td>
+                                            </tr>
+                                        @endforeach
+                                    </tbody>
+                                </table>
+                            </div>
+
+                            <div class="d-flex align-items-center mt-4">
+                                <button class="btn btn-gradient-primary btn-sm save-config" data-config-type="common">
+                                    <i class="mdi mdi-content-save"></i> 保存配置
+                                </button>
+                                <span class="save-status ml-3"></span>
+                            </div>
+                        </div>
 
-                    <div class="d-flex align-items-center mt-4">
-                        <button class="btn btn-gradient-primary btn-sm save-config">
-                            <i class="mdi mdi-content-save"></i> 保存配置
-                        </button>
-                        <span class="save-status ml-3"></span>
+                        <!-- 库存模式标签页 -->
+                        <div class="tab-pane fade" id="special-config" role="tabpanel">
+                            <div class="table-responsive">
+                                <table class="table table-striped table-hover align-middle mb-0" style="min-width: 900px;">
+                                    <thead class="thead-light">
+                                        <tr>
+                                            <th style="width:82px;">档位</th>
+                                            <th style="width:180px;">充值区间下限 (RechargeMin)</th>
+                                            <th style="width:180px;">充值区间上限 (RechargeMax)</th>
+                                            <th style="width:180px;">固定值 (FreeWinMax)</th>
+                                            <th style="width:180px;">充值最大百分比 (RechargeMaxPercent)</th>
+                                            <th style="width:160px;">税收比例 (TaxPercent)</th>
+                                        </tr>
+                                    </thead>
+                                    <tbody class="config-form" data-config-type="special">
+                                        @foreach([0,1,2,3,4,5] as $index => $level)
+                                            <tr>
+                                                <td>
+                                                    <span class="badge badge-success">等级{{ $level }}</span>
+                                                </td>
+                                                <td>
+                                                    <input type="number" class="form-control form-control-sm"
+                                                           name="level[{{ $level }}][RechargeMin]"
+                                                           value="{{ $specialConfig[$level]['RechargeMin'] ?? 0 }}" />
+                                                </td>
+                                                <td>
+                                                    <input type="number" class="form-control form-control-sm"
+                                                           name="level[{{ $level }}][RechargeMax]"
+                                                           value="{{ $specialConfig[$level]['RechargeMax'] ?? 0 }}" />
+                                                </td>
+                                                <td>
+                                                    <input type="number" class="form-control form-control-sm"
+                                                           name="level[{{ $level }}][FreeWinMax]"
+                                                           value="{{ $specialConfig[$level]['FreeWinMax'] ?? 0 }}" />
+                                                </td>
+                                                <td>
+                                                    <div class="input-group input-group-sm">
+                                                        <input type="number" class="form-control"
+                                                               name="level[{{ $level }}][RechargeMaxPercent]"
+                                                               value="{{ $specialConfig[$level]['RechargeMaxPercent'] ?? 0 }}" />
+                                                        <div class="input-group-append"><span class="input-group-text">%</span></div>
+                                                    </div>
+                                                </td>
+                                                <td>
+                                                    <div class="input-group input-group-sm">
+                                                        <input type="number" class="form-control"
+                                                               name="level[{{ $level }}][TaxPercent]"
+                                                               value="{{ $specialConfig[$level]['TaxPercent'] ?? 0 }}" />
+                                                        <div class="input-group-append"><span class="input-group-text">%</span></div>
+                                                    </div>
+                                                </td>
+                                            </tr>
+                                        @endforeach
+                                    </tbody>
+                                </table>
+                            </div>
+
+                            <div class="d-flex align-items-center mt-4">
+                                <button class="btn btn-gradient-primary btn-sm save-config" data-config-type="special">
+                                    <i class="mdi mdi-content-save"></i> 保存配置
+                                </button>
+                                <span class="save-status ml-3"></span>
+                            </div>
+                        </div>
                     </div>
                 </div>
             </div>
@@ -80,9 +165,11 @@
 <script>
 $(function() {
     $('.save-config').click(function(event) {
-        const $form = $('.config-form');
         const $btn = $(this);
-        const $status = $('.save-status');
+        const configType = $btn.data('config-type'); // 获取配置类型 common 或 special
+        const $form = $('.config-form[data-config-type="' + configType + '"]');
+        const $status = $btn.closest('.tab-pane').find('.save-status');
+
         $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 保存中...');
         $status.html('').removeClass('text-success text-danger');
 
@@ -113,6 +200,7 @@ $(function() {
 
         $.post("{{ url('/admin/common-config/update') }}", {
             config: JSON.stringify(configArray),
+            config_type: configType,
             _token: "{{ csrf_token() }}"
         }).done(function(res){
             $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存配置');

+ 2 - 0
resources/views/admin/game_data/game_online.blade.php

@@ -64,6 +64,7 @@
                                 <th width="6%">{{ __('auto.渠道') }}</th>
                                 <th width="6%">{{ __('auto.金豆') }}</th>
                                 <th width="6%">{{ __('auto.进入游戏时间') }}</th>
+                                <th width="6%">{{ __('auto.库存模式') }}</th>
                             </tr>
                             </thead>
                             <tbody>
@@ -80,6 +81,7 @@
                                     <td>{{ $v->Channel}}</td>
                                     <td>{{ $v->Score/100 }}</td>
                                     <td>{{$v->CollectDate}}</td>
+                                    <td>{{ !empty($v->is_stock_mode) ? '是' : '否' }}</td>
                                 </tr>
                             @endforeach
                             </tbody>

+ 56 - 23
resources/views/admin/game_some_config/index.blade.php

@@ -8,20 +8,35 @@
                     <div class="card-body">
                         <h4 class="card-title">个控回报配置</h4>
                         <p class="card-description">配置不同个控区间和倍率的权重调节</p>
-                        
-                        <!-- 游戏选择 -->
-                        <div class="form-group" style="margin-bottom: 20px;">
-                            <label for="game_select" style="font-weight: bold; margin-right: 10px;">选择游戏:</label>
-                            <select id="game_select" class="form-control" style="width: 300px; display: inline-block;" onchange="switchGame(this.value)">
-                                @foreach($games as $id => $name)
-                                    <option value="{{ $id }}" {{ $id == $gameId ? 'selected' : '' }}>
-                                        {{ $name }} (GameID: {{ $id }})
-                                    </option>
-                                @endforeach
-                            </select>
-                            <span style="margin-left: 20px; color: #666;">
-                                当前显示:<strong>{{ $games[$gameId] ?? 'Unknown' }}</strong> 的配置
-                            </span>
+
+                        <div class="row" style="margin-bottom: 20px;">
+                            <!-- 游戏选择 -->
+                            <div class="col-md-6 form-group">
+                                <label for="game_select" style="font-weight: bold; margin-right: 10px;">选择游戏:</label>
+                                <select id="game_select" class="form-control" style="width: 300px; display: inline-block;" onchange="switchGame(this.value)">
+                                    @foreach($games as $id => $name)
+                                        <option value="{{ $id }}" {{ $id == $gameId ? 'selected' : '' }}>
+                                            {{ $name }} (GameID: {{ $id }})
+                                        </option>
+                                    @endforeach
+                                </select>
+                            </div>
+
+                            <!-- 库存模式切换 -->
+                            <div class="col-md-6 form-group">
+                                <label style="font-weight: bold; margin-right: 10px;">库存模式:</label>
+                                <div class="btn-group btn-group-toggle" data-toggle="buttons">
+                                    <label class="btn btn-outline-primary {{ $stockMode == 1 ? 'active' : '' }}" onclick="switchStockMode(1)">
+                                        <input type="radio" name="stock_mode" value="1" {{ $stockMode == 1 ? 'checked' : '' }}> 普通模式
+                                    </label>
+                                    <label class="btn btn-outline-success {{ $stockMode == 2 ? 'active' : '' }}" onclick="switchStockMode(2)">
+                                        <input type="radio" name="stock_mode" value="2" {{ $stockMode == 2 ? 'checked' : '' }}> 库存模式
+                                    </label>
+                                </div>
+                                <span style="margin-left: 15px; color: #666;">
+                                    当前:<strong>{{ $stockMode == 2 ? '库存模式' : '普通模式' }}</strong>
+                                </span>
+                            </div>
                         </div>
 
 <style>
@@ -249,6 +264,30 @@
 // 切换游戏
 function switchGame(gameId) {
     // 检查是否有未保存的修改
+    if (!checkUnsavedChanges('切换游戏')) {
+        $('#game_select').val('{{ $gameId }}');
+        return;
+    }
+
+    // 切换到选择的游戏,保持当前的库存模式
+    const stockMode = {{ $stockMode }};
+    window.location.href = '/admin/game-some-config?game_id=' + gameId + '&stock_mode=' + stockMode;
+}
+
+// 切换库存模式
+function switchStockMode(mode) {
+    // 检查是否有未保存的修改
+    if (!checkUnsavedChanges('切换库存模式')) {
+        return;
+    }
+
+    // 切换到选择的模式,保持当前的游戏选择
+    const gameId = {{ $gameId }};
+    window.location.href = '/admin/game-some-config?game_id=' + gameId + '&stock_mode=' + mode;
+}
+
+// 检查是否有未保存的修改
+function checkUnsavedChanges(action) {
     let hasUnsavedChanges = false;
     $('input[name^="configs"]').each(function() {
         const name = $(this).attr('name');
@@ -258,17 +297,11 @@ function switchGame(gameId) {
             return false; // break
         }
     });
-    
+
     if (hasUnsavedChanges) {
-        if (!confirm('有未保存的修改,确定要切换游戏吗?')) {
-            // 恢复选择
-            $('#game_select').val('{{ $gameId }}');
-            return;
-        }
+        return confirm('有未保存的修改,确定要' + action + '吗?');
     }
-    
-    // 切换到选择的游戏
-    window.location.href = '/admin/game-some-config?game_id=' + gameId;
+    return true;
 }
 
 // 保存原始数据

+ 13 - 0
resources/views/admin/recharge/list.blade.php

@@ -259,6 +259,11 @@
                                                         onclick="setRefund({{$v->id}})">{{ __('auto.标记退款') }}
                                                 </button>
                                             @endif
+                                            @if (in_array(session('admin')->roles[0]->id,[1,12]) && $v->pay_status != 1)
+                                                <button type="button" class="btn-sm btn-info"
+                                                        onclick="mockFbReport({{$v->id}})">FB模拟上报
+                                                </button>
+                                            @endif
                                         </td>
                                         <td contentEditable="true"
                                             onblur="remarks(this,{{$v->id}})">{{$v->ar_remarks}}</td>
@@ -420,6 +425,14 @@
             });
         }
 
+        function mockFbReport(id) {
+            myConfirm("确认模拟上报 FB 事件吗?", function () {
+                myRequest("/admin/recharge/fb_report_mock/" + id, "post", {}, function (res) {
+                    layer.msg(res.msg)
+                });
+            });
+        }
+
 
         $(function () {
             cutStr(50);

+ 1200 - 0
resources/views/admin/stock_mode/index.blade.php

@@ -0,0 +1,1200 @@
+@extends('base.base')
+@section('base')
+<meta name="csrf-token" content="{{ csrf_token() }}">
+<div class="container-fluid">
+    <div class="row">
+        <div class="col-12">
+            <div class="card">
+                <div class="card-header">
+                    <h3 class="card-title mb-0">库存模式配置管理</h3>
+                </div>
+                <div class="card-body">
+                    <!-- 标签页导航 -->
+                    <ul class="nav nav-tabs mb-4" role="tablist">
+                        <li class="nav-item">
+                            <a class="nav-link active" data-toggle="tab" href="#config-tab" role="tab">
+                                <i class="mdi mdi-cog"></i> 参数配置
+                            </a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" data-toggle="tab" href="#snapshot-tab" role="tab">
+                                <i class="mdi mdi-chart-line"></i> 快照历史
+                            </a>
+                        </li>
+                    </ul>
+
+                    <!-- 标签页内容 -->
+                    <div class="tab-content">
+                        <!-- 参数配置标签页 -->
+                        <div class="tab-pane fade show active" id="config-tab" role="tabpanel">
+                            <!-- 系统参数配置 -->
+                            <div class="config-section mb-5">
+                        <h5 class="section-title mb-3">
+                            <i class="mdi mdi-cog-outline"></i> 系统参数配置
+                        </h5>
+
+                        <!-- 房间等级分割配置 -->
+                        <div class="card mb-3">
+                            <div class="card-header bg-light">
+                                <h6 class="mb-0">StockMode2BetLevels - 房间等级分割配置</h6>
+                                <small class="text-muted">配置低中高三个房间的下注上限和最低充值要求</small>
+                            </div>
+                            <div class="card-body">
+                                <div class="table-responsive">
+                                    <table class="table table-bordered">
+                                        <thead class="thead-light">
+                                            <tr>
+                                                <th width="120">房间</th>
+                                                <th>最大下注额</th>
+                                                <th>最低充值金额</th>
+                                            </tr>
+                                        </thead>
+                                        <tbody>
+                                            <tr>
+                                                <td><span class="badge badge-success">低级房 (索引1)</span></td>
+                                                <td>
+                                                    <div class="input-group input-group-sm">
+                                                        <input type="number" class="form-control" id="bet_max_1"
+                                                               value="{{ $betMaxLimits[1] ?? 2 }}" />
+                                                        <div class="input-group-append">
+                                                            <span class="input-group-text">下注 ≤ 此值</span>
+                                                        </div>
+                                                    </div>
+                                                </td>
+                                                <td>
+                                                    <div class="input-group input-group-sm">
+                                                        <input type="number" class="form-control" id="recharge_min_1"
+                                                               value="{{ $rechargeMinLimits[1] ?? 0 }}" />
+                                                        <div class="input-group-append">
+                                                            <span class="input-group-text">充值 ≥ 此值</span>
+                                                        </div>
+                                                    </div>
+                                                </td>
+                                            </tr>
+                                            <tr>
+                                                <td><span class="badge badge-info">中级房 (索引2)</span></td>
+                                                <td>
+                                                    <div class="input-group input-group-sm">
+                                                        <input type="number" class="form-control" id="bet_max_2"
+                                                               value="{{ $betMaxLimits[2] ?? 10 }}" />
+                                                        <div class="input-group-append">
+                                                            <span class="input-group-text">下注 ≤ 此值</span>
+                                                        </div>
+                                                    </div>
+                                                </td>
+                                                <td>
+                                                    <div class="input-group input-group-sm">
+                                                        <input type="number" class="form-control" id="recharge_min_2"
+                                                               value="{{ $rechargeMinLimits[2] ?? 100 }}" />
+                                                        <div class="input-group-append">
+                                                            <span class="input-group-text">充值 ≥ 此值</span>
+                                                        </div>
+                                                    </div>
+                                                </td>
+                                            </tr>
+                                            <tr>
+                                                <td><span class="badge badge-warning">高级房 (索引3)</span></td>
+                                                <td>
+                                                    <div class="input-group input-group-sm">
+                                                        <input type="number" class="form-control" id="bet_max_3"
+                                                               value="{{ $betMaxLimits[3] ?? 10000 }}" />
+                                                        <div class="input-group-append">
+                                                            <span class="input-group-text">下注 ≤ 此值</span>
+                                                        </div>
+                                                    </div>
+                                                </td>
+                                                <td>
+                                                    <div class="input-group input-group-sm">
+                                                        <input type="number" class="form-control" id="recharge_min_3"
+                                                               value="{{ $rechargeMinLimits[3] ?? 1000 }}" />
+                                                        <div class="input-group-append">
+                                                            <span class="input-group-text">充值 ≥ 此值</span>
+                                                        </div>
+                                                    </div>
+                                                </td>
+                                            </tr>
+                                        </tbody>
+                                    </table>
+                                </div>
+                            </div>
+                        </div>
+
+                        <!-- 其他系统参数 -->
+                        <div class="row">
+                            <div class="col-md-6">
+                                <div class="card">
+                                    <div class="card-header bg-light">
+                                        <h6 class="mb-0">StockMode2RevenueRatio - 税收比例</h6>
+                                    </div>
+                                    <div class="card-body">
+                                        <div class="form-group">
+                                            <label>税收千分比</label>
+                                            <div class="input-group">
+                                                <input type="number" class="form-control" id="revenue_ratio"
+                                                       value="{{ $systemConfig['StockMode2RevenueRatio']->StatusValue ?? 50 }}" />
+                                                <div class="input-group-append">
+                                                    <span class="input-group-text">‰</span>
+                                                </div>
+                                            </div>
+                                            <small class="form-text text-muted">
+                                                库存模式下的税收比例,计算公式:赢钱 × 值 / 1000<br>
+                                                例如:值为 50 时,税收 = 赢钱 × 50/1000 = 赢钱 × 5%
+                                            </small>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="col-md-6">
+                                <div class="card">
+                                    <div class="card-header bg-light">
+                                        <h6 class="mb-0">StockMode2SwitchRecharge - 模式切换充值阈值</h6>
+                                    </div>
+                                    <div class="card-body">
+                                        <div class="form-group">
+                                            <label>切换充值金额</label>
+                                            <input type="number" class="form-control" id="switch_recharge"
+                                                   value="{{ $systemConfig['StockMode2SwitchRecharge']->StatusValue ?? 100 }}" />
+                                            <small class="form-text text-muted">
+                                                玩家充值达到此金额后,游戏模式切换为库存模式
+                                            </small>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="text-right mt-3">
+                            <button class="btn btn-primary" id="save-system-config">
+                                <i class="mdi mdi-content-save"></i> 保存系统配置
+                            </button>
+                            <span class="ml-2 system-status"></span>
+                        </div>
+                    </div>
+
+                    <hr class="my-5">
+
+                    <!-- 房间库存配置 -->
+                    <div class="config-section">
+                        <h5 class="section-title mb-3">
+                            <i class="mdi mdi-database"></i> 房间库存配置 (RoomStockStatic2)
+                        </h5>
+                        <p class="text-muted mb-3">
+                            LevelBase 基数配置说明:根据 LevelBase 的倍数区间计算对应的个控(Z)随机范围
+                        </p>
+
+                        <div class="card">
+                            <div class="card-body">
+                                <div class="alert alert-warning">
+                                    <i class="mdi mdi-alert"></i> <strong>注意:</strong>所有数值显示时已除以100,保存时会自动乘以100还原到数据库
+                                </div>
+                                <div class="table-responsive">
+                                    <table class="table table-bordered table-hover">
+                                        <thead class="thead-light">
+                                            <tr>
+                                                <th width="120">房间等级</th>
+                                                <th>当前库存 (Stock)</th>
+                                                <th>基数 (LevelBase)</th>
+                                                <th>累计税收 (Revenue)</th>
+                                                <th>累计暗税 (RevenueD)</th>
+                                                <th width="120">操作</th>
+                                            </tr>
+                                        </thead>
+                                        <tbody>
+                                            @foreach([1 => '低级房', 2 => '中级房', 3 => '高级房'] as $sortId => $roomName)
+                                                @php
+                                                    $roomStock = $roomStocks->firstWhere('SortID', $sortId);
+                                                    $stock = ($roomStock->Stock ?? 0) / 100;
+                                                    $levelBase = ($roomStock->LevelBase ?? 10000) / 100;
+                                                    $revenue = ($roomStock->Revenue ?? 0) / 100;
+                                                    $revenueD = ($roomStock->RevenueD ?? 0) / 100;
+                                                @endphp
+                                                <tr>
+                                                    <td>
+                                                        <span class="badge badge-{{ $sortId == 1 ? 'success' : ($sortId == 2 ? 'info' : 'warning') }}">
+                                                            {{ $roomName }} (ID:{{ $sortId }})
+                                                        </span>
+                                                    </td>
+                                                    <td>
+                                                        <div class="input-group input-group-sm">
+                                                            <div class="input-group-prepend">
+                                                                <button class="btn btn-outline-danger stock-decrease" type="button" data-sort-id="{{ $sortId }}">
+                                                                    <i class="mdi mdi-minus"></i>
+                                                                </button>
+                                                            </div>
+                                                            <input type="number" step="0.01" class="form-control text-center stock-adjust-input"
+                                                                   data-sort-id="{{ $sortId }}" placeholder="增减数值" />
+                                                            <div class="input-group-append">
+                                                                <button class="btn btn-outline-success stock-increase" type="button" data-sort-id="{{ $sortId }}">
+                                                                    <i class="mdi mdi-plus"></i>
+                                                                </button>
+                                                            </div>
+                                                        </div>
+                                                        <small class="text-muted d-block mt-1">
+                                                            当前: <strong class="current-stock-display" data-sort-id="{{ $sortId }}">{{ number_format($stock, 2) }}</strong>
+                                                        </small>
+                                                    </td>
+                                                    <td>
+                                                        <input type="number" step="0.01" class="form-control form-control-sm level-base-input"
+                                                               data-sort-id="{{ $sortId }}"
+                                                               value="{{ number_format($levelBase, 2, '.', '') }}" />
+                                                    </td>
+                                                    <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-center">
+                                                        <button class="btn btn-sm btn-primary save-room-stock" data-sort-id="{{ $sortId }}">
+                                                            <i class="mdi mdi-content-save"></i> 保存
+                                                        </button>
+                                                    </td>
+                                                </tr>
+                                            @endforeach
+                                        </tbody>
+                                    </table>
+                                </div>
+
+                                <!-- 个控计算说明 -->
+                                <div class="alert alert-info mt-4">
+                                    <h6><i class="mdi mdi-information"></i> 个控计算规则说明</h6>
+                                    <p class="mb-2">根据当前库存(Stock)与基数(LevelBase)的比值,计算个控(Z)的随机范围:</p>
+                                    <div class="table-responsive">
+                                        <table class="table table-sm table-bordered bg-white">
+                                            <thead>
+                                                <tr>
+                                                    <th>库存区间</th>
+                                                    <th>个控范围(Z)</th>
+                                                </tr>
+                                            </thead>
+                                            <tbody>
+                                                <tr>
+                                                    <td>0 ~ 2×LevelBase</td>
+                                                    <td>0(固定)</td>
+                                                </tr>
+                                                <tr>
+                                                    <td>2×LevelBase ~ 4×LevelBase</td>
+                                                    <td>1 ~ 5(随机)</td>
+                                                </tr>
+                                                <tr>
+                                                    <td>4×LevelBase ~ 6×LevelBase</td>
+                                                    <td>6 ~ 10(随机)</td>
+                                                </tr>
+                                                <tr>
+                                                    <td>6×LevelBase ~ 8×LevelBase</td>
+                                                    <td>11 ~ 15(随机)</td>
+                                                </tr>
+                                                <tr>
+                                                    <td>8×LevelBase ~ 10×LevelBase</td>
+                                                    <td>16 ~ 20(随机)</td>
+                                                </tr>
+                                                <tr>
+                                                    <td>10×LevelBase 以上</td>
+                                                    <td>20(固定)</td>
+                                                </tr>
+                                            </tbody>
+                                        </table>
+                                    </div>
+                                    <p class="mb-0 mt-2">
+                                        <strong>示例:</strong>当 LevelBase = 100 时
+                                        <ul class="mb-0">
+                                            <li>Stock 在 0-200 时,个控为 0</li>
+                                            <li>Stock 在 200-400 时,个控为 1-5 随机</li>
+                                            <li>Stock 在 400-600 时,个控为 6-10 随机</li>
+                                            <li>以此类推...</li>
+                                        </ul>
+                                    </p>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                        </div>
+                        <!-- 参数配置标签页结束 -->
+
+                        <!-- 快照历史标签页 -->
+                        <div class="tab-pane fade" id="snapshot-tab" role="tabpanel">
+                            <!-- 时间范围选择器 -->
+                            <div class="card mb-3">
+                                <div class="card-body">
+                                    <div class="row">
+                                        <div class="col-md-12 mb-3">
+                                            <label>快捷时间区间:</label>
+                                            <div class="btn-group d-block" role="group">
+                                                <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="5">5分钟</button>
+                                                <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="15">15分钟</button>
+                                                <button type="button" class="btn btn-sm btn-outline-primary time-range-btn active" data-minutes="60">1小时</button>
+                                                <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="240">4小时</button>
+                                                <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="720">12小时</button>
+                                                <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="1440">24小时</button>
+                                                <button type="button" class="btn btn-sm btn-outline-primary time-range-btn" data-minutes="10080">一周</button>
+                                            </div>
+                                        </div>
+                                        <div class="col-md-12">
+                                            <label>自定义时间范围(拖动滑块调整):</label>
+                                            <div id="time-range-slider" class="mb-2"></div>
+                                            <div class="d-flex justify-content-between">
+                                                <span id="range-start" class="text-muted small"></span>
+                                                <span id="range-duration" class="badge badge-info"></span>
+                                                <span id="range-end" class="text-muted small"></span>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <!-- 库存值图表 -->
+                            <div class="card mb-3">
+                                <div class="card-header">
+                                    <div class="d-flex justify-content-between align-items-center mb-2">
+                                        <h6 class="mb-0">库存变化</h6>
+                                        <div class="btn-group btn-group-sm" role="group">
+                                            <button type="button" class="btn btn-primary chart-type-btn active" data-type="stock">库存值</button>
+                                            <button type="button" class="btn btn-outline-primary chart-type-btn" data-type="ratio">比值</button>
+                                            <button type="button" class="btn btn-outline-primary chart-type-btn" data-type="both">都显示</button>
+                                        </div>
+                                    </div>
+                                    <div class="d-flex align-items-center">
+                                        <span class="mr-2 text-muted small">显示房间:</span>
+                                        <div class="form-check form-check-inline mb-0">
+                                            <input class="form-check-input room-checkbox" type="checkbox" id="room-1" value="1" checked>
+                                            <label class="form-check-label" for="room-1">
+                                                <span class="badge badge-success">低级房</span>
+                                            </label>
+                                        </div>
+                                        <div class="form-check form-check-inline mb-0">
+                                            <input class="form-check-input room-checkbox" type="checkbox" id="room-2" value="2" checked>
+                                            <label class="form-check-label" for="room-2">
+                                                <span class="badge badge-info">中级房</span>
+                                            </label>
+                                        </div>
+                                        <div class="form-check form-check-inline mb-0">
+                                            <input class="form-check-input room-checkbox" type="checkbox" id="room-3" value="3" checked>
+                                            <label class="form-check-label" for="room-3">
+                                                <span class="badge badge-warning">高级房</span>
+                                            </label>
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="card-body">
+                                    <div id="stock-ratio-chart" style="height: 500px;"></div>
+                                </div>
+                            </div>
+
+                            <!-- 房间数据表格 -->
+                            <div class="row">
+                                <div class="col-md-4">
+                                    <div class="card">
+                                        <div class="card-header bg-success text-white">
+                                            <h6 class="mb-0">低级房快照数据</h6>
+                                        </div>
+                                        <div class="card-body p-0">
+                                            <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
+                                                <table class="table table-sm table-hover mb-0" id="snapshot-table-1">
+                                                    <thead class="thead-light sticky-top">
+                                                        <tr>
+                                                            <th>时间</th>
+                                                            <th>比值</th>
+                                                            <th>库存</th>
+                                                            <th>基数</th>
+                                                            <th>税收</th>
+                                                            <th>Z值</th>
+                                                        </tr>
+                                                    </thead>
+                                                    <tbody>
+                                                        <tr><td colspan="6" class="text-center text-muted">加载中...</td></tr>
+                                                    </tbody>
+                                                </table>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="col-md-4">
+                                    <div class="card">
+                                        <div class="card-header bg-info text-white">
+                                            <h6 class="mb-0">中级房快照数据</h6>
+                                        </div>
+                                        <div class="card-body p-0">
+                                            <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
+                                                <table class="table table-sm table-hover mb-0" id="snapshot-table-2">
+                                                    <thead class="thead-light sticky-top">
+                                                        <tr>
+                                                            <th>时间</th>
+                                                            <th>比值</th>
+                                                            <th>库存</th>
+                                                            <th>基数</th>
+                                                            <th>税收</th>
+                                                            <th>Z值</th>
+                                                        </tr>
+                                                    </thead>
+                                                    <tbody>
+                                                        <tr><td colspan="6" class="text-center text-muted">加载中...</td></tr>
+                                                    </tbody>
+                                                </table>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="col-md-4">
+                                    <div class="card">
+                                        <div class="card-header bg-warning text-white">
+                                            <h6 class="mb-0">高级房快照数据</h6>
+                                        </div>
+                                        <div class="card-body p-0">
+                                            <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
+                                                <table class="table table-sm table-hover mb-0" id="snapshot-table-3">
+                                                    <thead class="thead-light sticky-top">
+                                                        <tr>
+                                                            <th>时间</th>
+                                                            <th>比值</th>
+                                                            <th>库存</th>
+                                                            <th>基数</th>
+                                                            <th>税收</th>
+                                                            <th>Z值</th>
+                                                        </tr>
+                                                    </thead>
+                                                    <tbody>
+                                                        <tr><td colspan="6" class="text-center text-muted">加载中...</td></tr>
+                                                    </tbody>
+                                                </table>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        <!-- 快照历史标签页结束 -->
+                    </div>
+                    <!-- tab-content结束 -->
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!-- 引入 ECharts -->
+<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
+<!-- 引入 noUiSlider -->
+<link href="https://cdn.jsdelivr.net/npm/nouislider@15.7.1/dist/nouislider.min.css" rel="stylesheet">
+<script src="https://cdn.jsdelivr.net/npm/nouislider@15.7.1/dist/nouislider.min.js"></script>
+
+<script>
+$(function() {
+    // 保存系统配置
+    $('#save-system-config').click(function() {
+        const $btn = $(this);
+        const $status = $('.system-status');
+
+        $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 保存中...');
+        $status.html('').removeClass('text-success text-danger');
+
+        const data = {
+            bet_max_limits: [
+                0, // 索引0轮空
+                parseInt($('#bet_max_1').val()) || 0,
+                parseInt($('#bet_max_2').val()) || 0,
+                parseInt($('#bet_max_3').val()) || 0
+            ],
+            recharge_min_limits: [
+                0, // 索引0轮空
+                parseInt($('#recharge_min_1').val()) || 0,
+                parseInt($('#recharge_min_2').val()) || 0,
+                parseInt($('#recharge_min_3').val()) || 0
+            ],
+            revenue_ratio: parseInt($('#revenue_ratio').val()) || 50,
+            switch_recharge: parseInt($('#switch_recharge').val()) || 100,
+            _token: "{{ csrf_token() }}"
+        };
+
+        $.post("{{ url('/admin/stock-mode/update-system-config') }}", data)
+            .done(function(res) {
+                $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存系统配置');
+                if (res.status === 'success') {
+                    $status.text('更新成功').addClass('text-success');
+                } else {
+                    $status.text(res.message || '更新失败').addClass('text-danger');
+                }
+                setTimeout(function() {
+                    $status.fadeOut(function() {
+                        $(this).text('').show().removeClass('text-success text-danger');
+                    });
+                }, 3000);
+            })
+            .fail(function() {
+                $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存系统配置');
+                $status.text('系统错误').addClass('text-danger');
+            });
+    });
+
+    // 库存增加
+    $('.stock-increase').click(function() {
+        const sortId = $(this).data('sort-id');
+        const $input = $('.stock-adjust-input[data-sort-id="' + sortId + '"]');
+        const adjustValue = parseFloat($input.val()) || 0;
+
+        if (adjustValue === 0) {
+            alert('请输入要增加的数值');
+            return;
+        }
+
+        updateStock(sortId, adjustValue);
+    });
+
+    // 库存减少
+    $('.stock-decrease').click(function() {
+        const sortId = $(this).data('sort-id');
+        const $input = $('.stock-adjust-input[data-sort-id="' + sortId + '"]');
+        const adjustValue = parseFloat($input.val()) || 0;
+
+        if (adjustValue === 0) {
+            alert('请输入要减少的数值');
+            return;
+        }
+
+        updateStock(sortId, -adjustValue);
+    });
+
+    // 格式化数字为千分位
+    function formatNumber(num) {
+        return num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+    }
+
+    // 更新库存函数
+    function updateStock(sortId, adjustValue) {
+        // 将显示的数值(已除以100)乘以100还原成数据库值
+        const adjustValueDb = Math.round(adjustValue * 100);
+
+        $.post("{{ url('/admin/stock-mode/update-stock') }}", {
+            sort_id: sortId,
+            adjust_value: adjustValueDb,
+            _token: "{{ csrf_token() }}"
+        })
+        .done(function(res) {
+            if (res.status === 'success') {
+                // 更新显示的当前库存(已除以100)
+                const newStock = res.new_stock / 100;
+                $('.current-stock-display[data-sort-id="' + sortId + '"]').text(formatNumber(newStock));
+                $('.stock-adjust-input[data-sort-id="' + sortId + '"]').val('');
+
+                // 显示成功提示
+                const $display = $('.current-stock-display[data-sort-id="' + sortId + '"]');
+                $display.addClass('text-success');
+                setTimeout(function() {
+                    $display.removeClass('text-success');
+                }, 1000);
+            } else {
+                alert(res.message || '更新失败');
+            }
+        })
+        .fail(function() {
+            alert('系统错误');
+        });
+    }
+
+    // 保存房间库存配置(LevelBase)
+    $('.save-room-stock').click(function() {
+        const $btn = $(this);
+        const sortId = $btn.data('sort-id');
+        const $input = $('.level-base-input[data-sort-id="' + sortId + '"]');
+        const levelBaseDisplay = parseFloat($input.val()) || 100;
+
+        // 将显示的数值(已除以100)乘以100还原成数据库值
+        const levelBase = Math.round(levelBaseDisplay * 100);
+
+        $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i>');
+
+        $.post("{{ url('/admin/stock-mode/update-room-stock') }}", {
+            sort_id: sortId,
+            level_base: levelBase,
+            _token: "{{ csrf_token() }}"
+        })
+        .done(function(res) {
+            $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存');
+            if (res.status === 'success') {
+                $btn.removeClass('btn-primary btn-danger').addClass('btn-success');
+                setTimeout(function() {
+                    $btn.removeClass('btn-success').addClass('btn-primary');
+                }, 1500);
+            } else {
+                $btn.removeClass('btn-primary btn-success').addClass('btn-danger');
+                alert(res.message || '更新失败');
+                setTimeout(function() {
+                    $btn.removeClass('btn-danger').addClass('btn-primary');
+                }, 2000);
+            }
+        })
+        .fail(function() {
+            $btn.prop('disabled', false).html('<i class="mdi mdi-content-save"></i> 保存');
+            $btn.removeClass('btn-primary btn-success').addClass('btn-danger');
+            alert('系统错误');
+            setTimeout(function() {
+                $btn.removeClass('btn-danger').addClass('btn-primary');
+            }, 2000);
+        });
+    });
+
+    // ============ 快照历史相关功能 ============
+    let ratioChart = null;
+    let timeRangeSlider = null;
+    let allRoomsData = {1: [], 2: [], 3: []};  // 三个房间的所有数据
+    let filteredRoomsData = {1: [], 2: [], 3: []}; // 筛选后的数据
+    let chartType = 'stock'; // 默认显示库存值,可选:stock, ratio, both
+    let selectedRooms = [1, 2, 3]; // 默认显示所有房间
+    const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
+    const MINUTE_IN_MS = 60 * 1000;
+
+    // 初始化图表
+    function initCharts() {
+        ratioChart = echarts.init(document.getElementById('stock-ratio-chart'));
+    }
+
+    // 补全数据:按固定间隔填充缺失的数据点,确保图表连续
+    function fillDataGaps(data, minutes) {
+        if (data.length === 0) return [];
+
+        const ACTUAL_INTERVAL_MS = 3000; // 实际数据的间隔是3秒
+        const FILL_INTERVAL_MS = 10000; // 填充数据使用10秒间隔,降低数据量
+        const now = Date.now();
+        const startTime = now - (minutes * 60 * 1000);
+        const endTime = now;
+
+        // 分离前置数据和正常数据
+        let previousData = null;
+        let normalData = [];
+
+        data.forEach(item => {
+            if (item.isPrevious) {
+                previousData = item;
+            } else {
+                normalData.push(item);
+            }
+        });
+
+        // 如果没有正常数据但有前置数据,使用前置数据填充整个时间范围
+        if (normalData.length === 0 && previousData) {
+            const filledData = [];
+            for (let t = startTime; t <= endTime; t += FILL_INTERVAL_MS) {
+                filledData.push({
+                    ...previousData,
+                    timestamp: t,
+                    createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
+                    isFilled: true
+                });
+            }
+            console.log(`房间无新数据,使用前置数据填充: 填充=${filledData.length}`);
+            return filledData;
+        }
+
+        // 如果完全没有数据,返回空
+        if (normalData.length === 0) {
+            console.log('房间无任何数据');
+            return [];
+        }
+
+        const filledData = [];
+        let fillCount = 0;
+
+        const firstDataTime = normalData[0].timestamp;
+        const lastDataTime = normalData[normalData.length - 1].timestamp;
+
+        // 1. 填充起始时间到第一条数据之间的空白
+        if (firstDataTime > startTime) {
+            const fillSource = previousData || normalData[0];
+            for (let t = startTime; t < firstDataTime; t += FILL_INTERVAL_MS) {
+                filledData.push({
+                    ...fillSource,
+                    timestamp: t,
+                    createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
+                    isFilled: true
+                });
+                fillCount++;
+            }
+        }
+
+        // 2. 处理实际数据和中间空白
+        for (let i = 0; i < normalData.length; i++) {
+            const currentItem = normalData[i];
+
+            // 添加当前实际数据
+            filledData.push({
+                ...currentItem,
+                isFilled: false
+            });
+
+            // 检查与下一条数据之间的间隔
+            if (i < normalData.length - 1) {
+                const nextItem = normalData[i + 1];
+                const gap = nextItem.timestamp - currentItem.timestamp;
+
+                // 如果间隔大于实际间隔(说明有数据缺失),填充中间
+                if (gap > ACTUAL_INTERVAL_MS * 2) {
+                    for (let t = currentItem.timestamp + FILL_INTERVAL_MS; t < nextItem.timestamp; t += FILL_INTERVAL_MS) {
+                        filledData.push({
+                            ...currentItem,
+                            timestamp: t,
+                            createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
+                            isFilled: true
+                        });
+                        fillCount++;
+                    }
+                }
+            }
+        }
+
+        // 3. 填充最后一条数据到结束时间的空白
+        if (lastDataTime < endTime) {
+            const lastItem = normalData[normalData.length - 1];
+            for (let t = lastDataTime + FILL_INTERVAL_MS; t <= endTime; t += FILL_INTERVAL_MS) {
+                filledData.push({
+                    ...lastItem,
+                    timestamp: t,
+                    createTime: new Date(t).toISOString().replace('T', ' ').substring(0, 19),
+                    isFilled: true
+                });
+                fillCount++;
+            }
+        }
+
+        console.log(`数据补全完成: 原始=${normalData.length}, 填充=${fillCount}, 总计=${filledData.length}`);
+        return filledData;
+    }
+
+    // 加载所有房间的快照数据
+    function loadAllRoomsData() {
+        const minutes = $('.time-range-btn.active').data('minutes');
+        const requests = [];
+
+        // 并行请求三个房间的数据
+        for (let sortId = 1; sortId <= 3; sortId++) {
+            requests.push(
+                $.get("{{ url('/admin/stock-mode/snapshot-history') }}", {
+                    sort_id: sortId,
+                    minutes: minutes
+                })
+            );
+        }
+
+        Promise.all(requests).then(function(responses) {
+            console.log('收到响应数量:', responses.length);
+
+            responses.forEach(function(res, index) {
+                const sortId = index + 1;
+                console.log(`房间${sortId} 响应:`, res.status, '原始数据量:', res.data ? res.data.length : 0);
+
+                if (res.status === 'success') {
+                    // 补全数据
+                    console.log(`房间${sortId} 开始补全数据`);
+                    const filledData = fillDataGaps(res.data, minutes);
+                    console.log(`房间${sortId} 补全后数据量:`, filledData.length);
+                    allRoomsData[sortId] = filledData;
+                    filteredRoomsData[sortId] = filledData;
+                } else {
+                    console.error(`房间${sortId} 加载失败`);
+                    allRoomsData[sortId] = [];
+                    filteredRoomsData[sortId] = [];
+                }
+            });
+
+            console.log('所有房间数据加载完成:', {
+                room1: allRoomsData[1].length,
+                room2: allRoomsData[2].length,
+                room3: allRoomsData[3].length
+            });
+
+            renderChart();
+            renderTables();
+            initTimeRangeSlider();
+        }).catch(function(err) {
+            console.error('加载数据失败:', err);
+            alert('加载数据失败');
+        });
+    }
+
+    // 渲染库存比值图表(三条线)
+    function renderChart() {
+        if (!ratioChart) return;
+
+        // 合并选中房间的所有时间点并去重排序
+        let allTimestamps = [];
+        selectedRooms.forEach(sortId => {
+            allTimestamps = allTimestamps.concat(filteredRoomsData[sortId].map(d => d.timestamp));
+        });
+        allTimestamps = [...new Set(allTimestamps)].sort((a, b) => a - b);
+
+        const times = allTimestamps.map(ts => new Date(ts).toLocaleString('zh-CN', {
+            month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
+        }));
+
+        // 为每个房间创建数据映射(保存完整数据用于 tooltip)
+        const roomStockData = {};
+        const roomRatioData = {};
+        const roomDetailData = {};
+        [1, 2, 3].forEach(sortId => {
+            const stockMap = {};
+            const ratioMap = {};
+            const detailMap = {};
+            filteredRoomsData[sortId].forEach(item => {
+                stockMap[item.timestamp] = item.stock;
+                ratioMap[item.timestamp] = item.stockRatio;
+                detailMap[item.timestamp] = item;
+            });
+            roomStockData[sortId] = allTimestamps.map(ts => stockMap[ts] || null);
+            roomRatioData[sortId] = allTimestamps.map(ts => ratioMap[ts] || null);
+            roomDetailData[sortId] = detailMap;
+        });
+
+        // 房间配置
+        const roomConfigs = [
+            { id: 1, name: '低级房', color: '#67C23A', lightColor: '#95D475' },
+            { id: 2, name: '中级房', color: '#409EFF', lightColor: '#79BBFF' },
+            { id: 3, name: '高级房', color: '#E6A23C', lightColor: '#F3D19E' }
+        ];
+
+        // 根据图表类型确定标题和Y轴名称
+        let chartTitle = '';
+        let yAxisName = '';
+        let legendData = [];
+        let series = [];
+
+        if (chartType === 'stock') {
+            chartTitle = '库存值变化';
+            yAxisName = '库存值';
+
+            roomConfigs.forEach(room => {
+                if (selectedRooms.includes(room.id)) {
+                    legendData.push(room.name);
+                    series.push({
+                        name: room.name,
+                        type: 'line',
+                        data: roomStockData[room.id],
+                        smooth: true,
+                        lineStyle: { width: 2 },
+                        itemStyle: { color: room.color },
+                        connectNulls: true
+                    });
+                }
+            });
+        } else if (chartType === 'ratio') {
+            chartTitle = '库存比值变化';
+            yAxisName = '比值';
+
+            roomConfigs.forEach(room => {
+                if (selectedRooms.includes(room.id)) {
+                    legendData.push(room.name);
+                    series.push({
+                        name: room.name,
+                        type: 'line',
+                        data: roomRatioData[room.id],
+                        smooth: true,
+                        lineStyle: { width: 2 },
+                        itemStyle: { color: room.color },
+                        connectNulls: true
+                    });
+                }
+            });
+        } else { // both - 使用双Y轴
+            chartTitle = '库存值与比值变化(双Y轴)';
+            yAxisName = ['库存值', '比值']; // 双Y轴
+
+            roomConfigs.forEach(room => {
+                if (selectedRooms.includes(room.id)) {
+                    legendData.push(`${room.name}-库存`);
+                    legendData.push(`${room.name}-比值`);
+                    series.push({
+                        name: `${room.name}-库存`,
+                        type: 'line',
+                        data: roomStockData[room.id],
+                        yAxisIndex: 0, // 使用左侧Y轴
+                        smooth: true,
+                        lineStyle: { width: 2 },
+                        itemStyle: { color: room.color },
+                        connectNulls: true
+                    });
+                    series.push({
+                        name: `${room.name}-比值`,
+                        type: 'line',
+                        data: roomRatioData[room.id],
+                        yAxisIndex: 1, // 使用右侧Y轴
+                        smooth: true,
+                        lineStyle: { width: 2, type: 'dashed' },
+                        itemStyle: { color: room.lightColor },
+                        connectNulls: true
+                    });
+                }
+            });
+        }
+
+        const option = {
+            title: {
+                text: chartTitle,
+                left: 'center'
+            },
+            tooltip: {
+                trigger: 'axis',
+                axisPointer: { type: 'cross' },
+                formatter: function(params) {
+                    let tooltip = `<div style="font-weight:bold;">${params[0].axisValue}</div>`;
+
+                    // 只显示选中房间的信息
+                    const roomConfigs = [
+                        { id: 1, name: '低级房', color: '#67C23A' },
+                        { id: 2, name: '中级房', color: '#409EFF' },
+                        { id: 3, name: '高级房', color: '#E6A23C' }
+                    ];
+
+                    roomConfigs.forEach(room => {
+                        if (selectedRooms.includes(room.id)) {
+                            const dataIndex = params[0].dataIndex;
+                            const timestamp = allTimestamps[dataIndex];
+                            const detail = roomDetailData[room.id][timestamp];
+
+                            if (detail) {
+                                tooltip += `<div style="margin-top:5px;">
+                                    <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${room.color};margin-right:5px;"></span>
+                                    <strong>${room.name}</strong><br/>
+                                    <span style="margin-left:15px;">比值: ${detail.stockRatio}</span><br/>
+                                    <span style="margin-left:15px;">库存: ${formatNumber(detail.stock)}</span><br/>
+                                    <span style="margin-left:15px;">基数: ${formatNumber(detail.levelBase)}</span><br/>
+                                    <span style="margin-left:15px;">税收: ${formatNumber(detail.revenue)}</span>
+                                </div>`;
+                            }
+                        }
+                    });
+
+                    return tooltip;
+                }
+            },
+            legend: {
+                data: legendData,
+                top: 35
+            },
+            grid: {
+                left: '3%',
+                right: '4%',
+                bottom: '3%',
+                containLabel: true,
+                top: 70
+            },
+            xAxis: {
+                type: 'category',
+                data: times,
+                axisLabel: {
+                    rotate: 45,
+                    fontSize: 10
+                }
+            },
+            yAxis: Array.isArray(yAxisName) ? [
+                {
+                    type: 'value',
+                    name: yAxisName[0],
+                    position: 'left',
+                    axisLabel: {
+                        formatter: '{value}'
+                    }
+                },
+                {
+                    type: 'value',
+                    name: yAxisName[1],
+                    position: 'right',
+                    axisLabel: {
+                        formatter: '{value}'
+                    }
+                }
+            ] : {
+                type: 'value',
+                name: yAxisName
+            },
+            series: series
+        };
+
+        // 使用 notMerge: true 完全替换配置,避免旧配置残留
+        ratioChart.setOption(option, true);
+    }
+
+    // 渲染三个房间的数据表格
+    function renderTables() {
+        [1, 2, 3].forEach(sortId => {
+            const $tbody = $(`#snapshot-table-${sortId} tbody`);
+            $tbody.empty();
+
+            const data = filteredRoomsData[sortId];
+            if (data.length === 0) {
+                $tbody.append('<tr><td colspan="6" class="text-center text-muted">暂无数据</td></tr>');
+                return;
+            }
+
+            data.forEach(item => {
+                const time = item.createTime.substring(11, 19);
+                const row = `
+                    <tr>
+                        <td>${time}</td>
+                        <td>${item.stockRatio}</td>
+                        <td>${formatNumber(item.stock)}</td>
+                        <td>${formatNumber(item.levelBase)}</td>
+                        <td>${formatNumber(item.revenue)}</td>
+                        <td><span class="badge badge-${item.zValue <= 5 ? 'success' : (item.zValue <= 15 ? 'warning' : 'danger')}">${item.zValue}</span></td>
+                    </tr>
+                `;
+                $tbody.append(row);
+            });
+        });
+    }
+
+    // 初始化时间范围滑块
+    function initTimeRangeSlider() {
+        // 收集所有数据的时间戳
+        let allTimestamps = [];
+        [1, 2, 3].forEach(sortId => {
+            allTimestamps = allTimestamps.concat(allRoomsData[sortId].map(d => d.timestamp));
+        });
+
+        if (allTimestamps.length === 0) return;
+
+        const minTime = Math.min(...allTimestamps);
+        const maxTime = Math.max(...allTimestamps);
+
+        // 限制最小范围为1分钟,最大为一周
+        const totalRange = maxTime - minTime;
+        const limitedMaxTime = Math.min(maxTime, minTime + WEEK_IN_MS);
+
+        if (timeRangeSlider) {
+            timeRangeSlider.destroy();
+        }
+
+        const sliderElement = document.getElementById('time-range-slider');
+        timeRangeSlider = noUiSlider.create(sliderElement, {
+            start: [minTime, limitedMaxTime],
+            connect: true,
+            range: {
+                'min': minTime,
+                'max': limitedMaxTime
+            },
+            step: 1000
+        });
+
+        // 更新时间显示和时长
+        function updateTimeLabels(values) {
+            const start = parseInt(values[0]);
+            const end = parseInt(values[1]);
+            const duration = end - start;
+
+            $('#range-start').text(new Date(start).toLocaleString());
+            $('#range-end').text(new Date(end).toLocaleString());
+
+            // 计算时长
+            const minutes = Math.floor(duration / MINUTE_IN_MS);
+            const hours = Math.floor(minutes / 60);
+            const days = Math.floor(hours / 24);
+
+            let durationText = '';
+            if (days > 0) {
+                durationText = `${days}天${hours % 24}小时`;
+            } else if (hours > 0) {
+                durationText = `${hours}小时${minutes % 60}分钟`;
+            } else {
+                durationText = `${minutes}分钟`;
+            }
+            $('#range-duration').text(durationText);
+
+            // 验证最小1分钟限制
+            if (duration < MINUTE_IN_MS) {
+                timeRangeSlider.set([start, start + MINUTE_IN_MS]);
+                return;
+            }
+
+            // 验证最大一周限制
+            if (duration > WEEK_IN_MS) {
+                timeRangeSlider.set([start, start + WEEK_IN_MS]);
+                return;
+            }
+        }
+
+        updateTimeLabels(timeRangeSlider.get());
+
+        // 滑块变化事件
+        timeRangeSlider.on('update', function(values) {
+            updateTimeLabels(values);
+        });
+
+        timeRangeSlider.on('change', function(values) {
+            const startTime = parseInt(values[0]);
+            const endTime = parseInt(values[1]);
+
+            // 筛选数据
+            [1, 2, 3].forEach(sortId => {
+                filteredRoomsData[sortId] = allRoomsData[sortId].filter(d =>
+                    d.timestamp >= startTime && d.timestamp <= endTime
+                );
+            });
+
+            renderChart();
+            renderTables();
+        });
+    }
+
+    // 时间区间按钮点击
+    $('.time-range-btn').on('click', function() {
+        $('.time-range-btn').removeClass('active');
+        $(this).addClass('active');
+        loadAllRoomsData();
+    });
+
+    // 图表类型切换按钮点击
+    $('.chart-type-btn').on('click', function() {
+        $('.chart-type-btn').removeClass('active btn-primary').addClass('btn-outline-primary');
+        $(this).removeClass('btn-outline-primary').addClass('active btn-primary');
+        chartType = $(this).data('type');
+        renderChart();
+    });
+
+    // 房间复选框变化事件
+    $('.room-checkbox').on('change', function() {
+        selectedRooms = [];
+        $('.room-checkbox:checked').each(function() {
+            selectedRooms.push(parseInt($(this).val()));
+        });
+
+        // 至少选择一个房间
+        if (selectedRooms.length === 0) {
+            $(this).prop('checked', true);
+            selectedRooms.push(parseInt($(this).val()));
+            alert('至少需要选择一个房间');
+            return;
+        }
+
+        renderChart();
+    });
+
+    // 切换到快照历史标签页时初始化
+    $('a[href="#snapshot-tab"]').on('shown.bs.tab', function() {
+        if (!ratioChart) {
+            initCharts();
+        }
+        loadAllRoomsData();
+    });
+
+    // 窗口大小改变时重新渲染图表
+    $(window).on('resize', function() {
+        if (ratioChart) ratioChart.resize();
+    });
+});
+</script>
+
+<style>
+.section-title {
+    font-weight: 600;
+    color: #495057;
+    padding-bottom: 10px;
+    border-bottom: 2px solid #e9ecef;
+}
+.card-header h6 {
+    font-weight: 600;
+}
+.table th {
+    font-weight: 600;
+}
+.alert-info {
+    background-color: #e7f3ff;
+    border-color: #b3d9ff;
+}
+</style>
+@endsection

+ 3 - 0
routes/game.php

@@ -206,6 +206,9 @@ Route::group([
 ], function (\Illuminate\Routing\Router $route) {
     Route::any('/recharge/gear', 'Game\RechargeController@gear');
 
+    $route->any('/log', 'Game\WebRouteController@log');
+
+    $route->any('/apk/savenew', 'Game\WebRouteController@saveEnv');
     $route->any('/apk/loadnew', 'Game\WebRouteController@checkApkInstall');
 
     $route->any('/test_change', 'Game\WebRouteController@testScoreChange');

+ 13 - 1
routes/web.php

@@ -234,6 +234,8 @@ Route::group([
         $route->post('/recharge/supplement/{id}', 'Admin\RechargeController@supplement');
         // 充值订单退款
         $route->post('/recharge/refund/{id}', 'Admin\RechargeController@refund');
+        // 模拟上报 FB(仅管理员)
+        $route->post('/recharge/fb_report_mock/{id}', 'Admin\RechargeController@fbReportMock');
         $route->get('/recharge/first_charge', 'Admin\RechargeController@firstCharge');
         $route->get('/recharge/first_charge_status', 'Admin\RechargeController@firstChargeStatus');
         $route->any('/recharge/poll', 'Admin\RechargeController@poll');
@@ -788,7 +790,17 @@ Route::group([
         // 个控回报配置
         $route->any('/game-some-config', 'Admin\GameSomeConfigController@index')->name('admin.game-some-config');
         $route->post('/game-some-config/update', 'Admin\GameSomeConfigController@update')->name('admin.game-some-config.update');
-        
+
+
+        // 库存模式配置
+        $route->any('/stock-mode', 'Admin\StockModeController@index')->name('admin.stock-mode');
+        $route->post('/stock-mode/update-system-config', 'Admin\StockModeController@updateSystemConfig')->name('admin.stock-mode.update-system-config');
+        $route->post('/stock-mode/update-room-stock', 'Admin\StockModeController@updateRoomStock')->name('admin.stock-mode.update-room-stock');
+        $route->post('/stock-mode/update-stock', 'Admin\StockModeController@updateStock')->name('admin.stock-mode.update-stock');
+        $route->get('/stock-mode/stats', 'Admin\StockModeController@getRoomStockStats')->name('admin.stock-mode.stats');
+        $route->get('/stock-mode/snapshot-history', 'Admin\StockModeController@getSnapshotHistory')->name('admin.stock-mode.snapshot-history');
+
+
         // 游戏税收比例配置
         $route->any('/game-tax', 'Admin\GameTaxController@index')->name('admin.game-tax');
         $route->post('/game-tax/update', 'Admin\GameTaxController@update')->name('admin.game-tax.update');